Skip to content

24. 即时通讯

24.1 什么是即时通讯​

即时通讯(Instant messaging,简称 IM)通常是指互联网上用以进行实时通讯的系统,允许两人或多人使用网络即时的传递文字信息、文档、语音与视频交流。

即时通讯不同于 E-mail 在于它的交谈是实时的。大部分的即时通讯服务提供了状态信息的特性 ── 显示联络人名单,联络人是否在线上与能否与联络人交谈。

在互联网上目前使用较广的即时通讯服务包括 Windows Live Messenger、AOL Instant Messenger、Skype、Yahoo! Messenger、NET Messenger Service、Jabber、ICQ 与 QQ 等。

24.2 即时通讯应用场景​

即时通讯应用场景非常广泛,需要实时交互消息的都需要。如:

  • 聊天工具:QQ、WeChat、在线客服等
  • 手游网游:王者荣耀、魔兽等
  • 网络直播:腾讯课堂、抖音直播等
  • 订单推送:美团、餐饮下单系统等
  • 协同办公:公司内部文件分享、工作安排、在线会议等。

以上只是列举了比较常用的应用场景,但即时通讯的作用远不止于此。

24.3 关于 SignalR​

即时通讯技术实现是复杂且过于底层化,所以微软为了简化即时通讯应用程序,开发出了一个强大且简易使用的通信库:SignalR,通过该库我们可以轻松实现类似 QQ、微信这类 IM 聊天工具,也能快速实现消息推送、订单推送这样的系统。

24.3.1 微软官方介绍​

ASP.NET Core SignalR 是一种开放源代码库,可简化将实时 web 功能添加到应用程序的功能。 实时 web 功能使服务器端代码可以立即将内容推送到客户端。

适用于 SignalR :

  • 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
  • 仪表板和监视应用。 示例包括公司仪表板、即时销售更新或旅行警报。
  • 协作应用。 协作应用的示例包括白板应用和团队会议软件。
  • 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。

目前 SignalR 已经内置在 .NET 5 SDK 中。同时 SignalR 支持 Web、App、Console、Desktop 等多个应用平台。

24.4 注册 SignalR 服务​

在 Furion 框架中,任何服务功能都需要先注册后再使用,SignalR 也不例外。只需要在 Startup.cs 中添加注册即可:

  • .NET5
using Microsoft.AspNetCore.Builder;  
using Microsoft.AspNetCore.Hosting;  
using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  

namespace Furion.Web.Core  
{  
    public sealed class Startup : AppStartup  
    {  
        public void ConfigureServices(IServiceCollection services)  
        {  
            // 其他代码...  

            // 添加即时通讯  
            services.AddSignalR();  
        }  

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
        {  
            // 其他代码...  

            app.UseEndpoints(endpoints =>  
            {  
                // 注册集线器  
                endpoints.MapHubs();  

                endpoints.MapControllerRoute(  
                    name: "default",  
                    pattern: "{controller=Home}/{action=Index}/{id?}");  
            });  
        }  
    }  
}  

  • .NET6+
var builder = WebApplication.CreateBuilder(args).Inject();  

// 其他代码...  

// 添加即时通讯  
builder.Services.AddSignalR();  

var app = builder.Build();  

// 其他代码  

// 注册集线器  
app.MapHubs();  

app.MapControllers();  

app.Run();  

24.5 SignalR 长连接和集线器​

SignalR 包含两种用于在客户端和服务器之间进行通信的模型:持久性连接和 集线器 中心。

24.5.1 持久性连接​

连接表示用于发送单接收方、分组或广播消息的简单终结点。 持久性连接 (在 .NET 代码中由 PersistentConnection 类表示,在 ASP.NET Core SignalR 中 ,PersistentConnection 类已被删除。) 使开发人员能够直接访问 SignalR 公开的低级别通信协议。 使用基于连接的 Api (如 Windows Communication Foundation)的开发人员将对使用连接通信模型非常熟悉。

24.5.2 集线器​

集线器是一种基于连接 API 构建的更高级别管道,它允许客户端和服务器直接调用方法。 SignalR 就像魔术一样处理跨机器边界的调度,使客户端能够像本地方法一样轻松地调用服务器上的方法,反之亦然。 如果开发人员已使用远程调用 (如 .NET 远程处理),则将对使用中心通信模型非常熟悉。 使用集线器还可以将强类型参数传递给方法,从而启用模型绑定。

小知识想了解更多关于 持久性连接 和 集线器中心 可查阅 SignalR 官方文档

24.6 集线器 Hub 定义​

在本章节中主要推荐使用集线器通信模型方式。 这里主要说明 Hub 定义,如果无法理解该通信模型的作用也没关系,接下来的例子会带大家慢慢熟悉并使用。

24.6.1 两种定义方式​

定义集线器只需要继承 Hub 或 Hub<TStrongType> 泛型基类即可,如:

  • Hub 方式
using Furion.InstantMessaging;  
using Microsoft.AspNetCore.SignalR;  

namespace Furion.Core  
{  
    /// <summary>  
    /// 聊天集线器  
    /// </summary>  
    public class ChatHub : Hub  
    {  
        // 定义一个方法供客户端调用  
        public Task SendMessage(string user, string message)  
        {  
            // 触发客户端定义监听的方法  
            return Clients.All.SendAsync("ReceiveMessage", user, message);  
        }  
    }  
}  

  • Hub<TStrongType> 类型方式
public interface IChatClient  
{  
    Task ReceiveMessage(string user, string message);  
}  

public class StronglyTypedChatHub : Hub<IChatClient>  
{  
    // 定义一个方法供客户端调用  
    public async Task SendMessage(string user, string message)  
    {  
        // 触发客户端定义监听的方法  
        await Clients.All.ReceiveMessage(user, message);  
    }  
}  

通过使用 Hub<IChatClient> 可以对客户端方法进行编译时检查。 这可以防止由于使用神奇字符串而导致的问题,因为 Hub<T> 只能提供对在接口中定义的方法的访问。

24.6.2 [MapHub] 配置连接地址​

在 SignalR 库中要求每一个公开的集线器都需要配置客户端连接地址,所以,Furion 框架提供了简便的 [MapHub] 配置,如:

using Furion.InstantMessaging;  
using Microsoft.AspNetCore.SignalR;  
using System;  
using System.Threading.Tasks;  

namespace Furion.Core  
{  
    /// <summary>  
    /// 聊天集线器  
    /// </summary>  
    [MapHub("/hubs/chathub")]  
    public class ChatHub : Hub  
    {  
        // ...  
    }  
}  

SignalR 原生配置方式在 Furion 中推荐使用 [MapHub] 方式配置集线器客户端连接地址,当然也可以使用 SignalR 提供的方式,如在 Startup.cs 配置:

public sealed class Startup : AppStartup  
{  
   // 其他代码  
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
    {  
        // 其他代码...  
        app.UseEndpoints(endpoints =>  
        {  
            // 注册集线器  
            endpoints.MapHub<ChatHub>("/hubs/chathub");  
        });  
    }  
}  

24.6.3 Hub 注册更多配置​

有些时候,我们需要注册 Hub 时配置更多参数,比如权限、跨域等,这时只需要在 Hub 派生类中编写以下静态方法即可:

using Furion.InstantMessaging;  
using Microsoft.AspNetCore.SignalR;  
using System;  
using System.Threading.Tasks;  

namespace Furion.Core  
{  
    [MapHub("/hubs/chathub")]  
    public class ChatHub : Hub  
    {  
        // 其他代码  

        public static void HttpConnectionDispatcherOptionsSettings(HttpConnectionDispatcherOptions options)  
        {  
            // 配置  
        }  

        public static void HubEndpointConventionBuilderSettings(HubEndpointConventionBuilder Builder)  
        {  
            // 配置  
        }  
    }  
}  

以上配置等价于 SignalR 在 Startup.cs 中的配置:

app.UseEndpoints(endpoints =>  
{  
    var builder = endpoints.MapHub<ChatHub>("/hubs/chathub", options =>  
       {  
           // 配置  
       });  
});  

24.7 获取 Hub 实例方式​

SignalR 提供了几种方式进行获取 Hub 实例。

24.7.1 IHubContext 注入方式​

IHubContext 默认注册为单例模式,可在任何地方直接获取实例。

public class HomeController : Controller  
{  
    private readonly IHubContext<NotificationHub> _hubContext;  

    public HomeController(IHubContext<NotificationHub> hubContext)  
    {  
        _hubContext = hubContext;  
    }  

    public async Task<IActionResult> Index()  
    {  
        await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at: {DateTime.Now}");  
        return View();  
    }  
}  

24.7.2 HttpContext 解析方式​

 var hubContext = context.RequestServices  
                         .GetRequiredService<IHubContext<ChatHub>>();  

24.7.3 IHost 中解析方式​

public class Program  
{  
    public static void Main(string[] args)  
    {  
        var host = CreateHostBuilder(args).Build();  
        var hubContext = host.Services.GetService(typeof(IHubContext<ChatHub>));  
        host.Run();  
    }  

    public static IHostBuilder CreateHostBuilder(string[] args) =>  
        Host.CreateDefaultBuilder(args)  
            .ConfigureWebHostDefaults(webBuilder => {  
                webBuilder.UseStartup<Startup>();  
            });  
}  

24.7.4 强类型 IHubContext 注入方式​

默认情况下,IHubContext 非泛型实例返回的是 dynamic 动态类型对象,该类型对象无法获得编译期语法检查和 IDE 智能提示,所以我们可以传入一个和自定义 Hub 一样的方法签名接口,如:

public class ChatController : Controller  
{  
    public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }  

    public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)  
    {  
        _strongChatHubContext = chatHubContext;  
    }  

    public async Task SendMessage(string user, string message)  
    {  
        await _strongChatHubContext.Clients.All.ReceiveMessage(user, message);  
    }  
}  

24.7.5 IHubContext 泛型转换​

正常情况下,我们获取的是 IHubContext<> 的实例,但在一些反射场景下,可以将 IHubContext<> 强制转换成 IHubContext 从而更易于操作,如:

var myHubContext = context.RequestServices  
                          .GetRequiredService<IHubContext<MyHub>>();  
var myOtherHubContext = context.RequestServices  
                               .GetRequiredService<IHubContext<MyOtherHub>>();  

await CommonHubContextMethod((IHubContext)myHubContext);  
await CommonHubContextMethod((IHubContext)myOtherHubContext);  

24.8 服务端和客户端双工通信​

24.8.1 触发所有客户端代码​

Clients.All.客户端方法(参数);  

24.8.2 触发调用者客户端​

Clients.Caller.客户端方法(参数);  

24.8.3 触发除了调用者以外的客户端​

Clients.Others.客户端方法(参数);  

24.8.4 触发特定用户客户端​

Clients.User("用户").客户端方法(参数);  

24.8.5 触发多个用户客户端​

Clients.Users("用户","用户2",...).客户端方法(参数);  

24.8.6 触发分组内客户端​

Clients.Group("分组").客户端方法(参数);  

24.8.7 触发多个分组客户端​

Clients.Groups("分组","分组2",...).客户端方法(参数);  

24.8.8 触发分组外的客户端​

Clients.GroupExcept("分组").客户端方法(参数);  

24.9 自定义用户唯一标识​

默认情况下 SignalR 会为每一个链接创建 ConnectionId,但是这个 ConnectionId 并没有和我们系统的用户绑关联起来,所以需要采用自定义 ConnectionId,如:

public class YourUserIdProvider : IUserIdProvider  
{  
    public virtual string GetUserId(HubConnectionContext connection)  
    {  
        // 你如何获取 UserId,可以通过 connection.User 获取 JWT 授权的用户  
    }  
}  

然后在 Startup.cs 中注册即可:

builder.Services.AddSingleton<IUserIdProvider, YourUserIdProvider>();  

之后就可以通过自定义 UserId 发送消息:

Clients.User(userId).客户端方法(参数);  

查看更多文档 https://docs.microsoft.com/zh-cn/aspnet/core/signalr/authn-and-authz?view\=aspnetcore-6.0#use-claims-to-customize-identity-handling

24.10 分组管理​

整理中...

24.11 各个客户端连接 API​

24.11.1 JavaScript 客户端​

  1. 安装 @microsoft/signalr 包
npm init -y  
npm install @microsoft/signalr  

也可以通过 script 的方式引入,如:

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>  

  1. 基本使用
const connection = new signalR.HubConnectionBuilder()  
  .withUrl("/chathub")  
  .configureLogging(signalR.LogLevel.Information)  
  .build();  

async function start() {  
  try {  
    await connection.start();  
    console.log("SignalR Connected.");  
  } catch (err) {  
    console.log(err);  
    setTimeout(start, 5000);  
  }  
}  

connection.onclose(async () => {  
  await start();  
});  

// Start the connection.  
start();  

了解更多:https://learn.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view\=aspnetcore-8.0\&tabs\=visual-studio

24.11.2 TypeScript 客户端​

在 vue3.2+ 中使用​

  1. 安装微软的 signalr typescript 客户端包,主要用于调用服务端方法,如( Hub 中的 SendMessage 方法):
npm i @microsoft/signalr @types/node  

  1. 示例代码
import { HubConnectionBuilder } from "@microsoft/signalr";  

<script setup lang="ts">  
    import { HubConnectionBuilder } from "@microsoft/signalr";  
    import { ref } from "vue";  

    const messages = ref('');  
    const reciveMessage = (msg: string) => {  
        console.log("msg", msg);  
    }  

    //初始化signalr HubConnection对象  
    const connection = new HubConnectionBuilder()  
        .withUrl("<你的signalr连接地址>")//https://localhost:7260/chatHub  
        .build();  

    //启动连接并发送消息测试  
    connection.start()  
              .then(() => connection.send("SendMessage", "Hello"));  

    //注册web端方法以供后端调用  
    connection.on("ReciveMessage", reciveMessage);  

    const sendMsg = async () => {  
        console.log(messages.value);  
        await connection.send("SendMessage", messages.value).catch(function (err) {  
            console.log(err);  
        });  
    }  
</script>  

参考文档

24.11.3 .NET 客户端​

  1. 安装 Microsoft.AspNetCore.SignalR.Client 包
Install-Package Microsoft.AspNetCore.SignalR.Client  

  1. 基本使用
using System;  
using System.Threading.Tasks;  
using System.Windows;  
using Microsoft.AspNetCore.SignalR.Client;  

namespace SignalRChatClient  
{  
    public partial class MainWindow : Window  
    {  
        HubConnection connection;  
        public MainWindow()  
        {  
            InitializeComponent();  

            connection = new HubConnectionBuilder()  
                .WithUrl("http://localhost:53353/ChatHub")  
                .Build();  

            connection.Closed += async (error) =>  
            {  
                await Task.Delay(new Random().Next(0,5) * 1000);  
                await connection.StartAsync();  
            };  
        }  

        private async void connectButton_Click(object sender, RoutedEventArgs e)  
        {  
            connection.On<string, string>("ReceiveMessage", (user, message) =>  
            {  
                this.Dispatcher.Invoke(() =>  
                {  
                   var newMessage = $"{user}: {message}";  
                   messagesList.Items.Add(newMessage);  
                });  
            });  

            try  
            {  
                await connection.StartAsync();  
                messagesList.Items.Add("Connection started");  
                connectButton.IsEnabled = false;  
                sendButton.IsEnabled = true;  
            }  
            catch (Exception ex)  
            {  
                messagesList.Items.Add(ex.Message);  
            }  
        }  

        private async void sendButton_Click(object sender, RoutedEventArgs e)  
        {  
            try  
            {  
                await connection.InvokeAsync("SendMessage",  
                    userTextBox.Text, messageTextBox.Text);  
            }  
            catch (Exception ex)  
            {  
                messagesList.Items.Add(ex.Message);  
            }  
        }  
    }  
}  

了解更多使用:https://learn.microsoft.com/zh-cn/aspnet/core/signalr/dotnet-client?view\=aspnetcore-8.0\&tabs\=visual-studio

24.11.4 Java 客户端​

  1. 安装 com.microsoft.signalr:signalr:7.0.0 包

  2. Gradle 方式

implementation 'com.microsoft.signalr:signalr:7.0.0'  

  • Maven 方式
<dependency>  
    <groupId>com.microsoft.signalr</groupId>  
    <artifactId>signalr</artifactId>  
    <version>1.0.0</version>  
</dependency>  

  1. 基本使用
// 连接  
HubConnection hubConnection = HubConnectionBuilder.create(input)  
        .build();  

// 调用服务端代码  
hubConnection.send("Send", input);  

// 服务端调用客户端代码  
hubConnection.on("Send", (message) -> {  
    System.out.println("New Message: " + message);  
}, String.class);  

了解更多:https://learn.microsoft.com/zh-cn/aspnet/core/signalr/java-client?view\=aspnetcore-8.0

24.12 反馈与建议​

与我们交流给 Furion 提 Issue。


了解更多想了解更多 SignalR 知识可查阅 SignalR 官方文档 或 ASP.NET Core SignalR 章节。